Skip to content

同事分享-ZGC 调优建议

ZGC垃圾回收器:原理与实践优化

一、问题背景

Hinton 测试环境发生 OOM 连续重启,经过排查是因为堆内存超出容器内存限制,导致容器的 OOM 直接被杀死,因此也没有机会执行 java-dump。

在排查过程中发现一个奇妙的现象:年轻代对象总是在临近内存打满时才触发回收(如图1所示)

image.png

在重新配置容器内存解决 OOM 问题后,开始排查出现上述现象的原因。

二、ZGC背景

基本概念

不同于 G1,JDK 21 之前 ZGC 是无分代的,到 JDK 21 起才正式支持分代,并且计划在 JDK 23 成为默认垃圾回收器,在此之前都需要手动指定使用分代 ZGC(-XX:+UseZGC)。

TIPS:启动分代 ZGC 参数(-XX:+ZGenerational

分代回收理论基础

分代回收主要是基于两个假说:

  • 弱分代假说(Weak Generational Hypothesis):绝大多数对象是朝生夕死,在年轻时死亡。
  • 强分代假说(Strong Generational Hypothesis):熬过多次垃圾回收的老年对象更难死亡。

收集年轻对象消耗的资源较少,回收内存较多;收集老年对象消耗的资源较多,回收内存较少。

分代 ZGC(Generational ZGC)特性

  1. 极低的停顿时间:其GC暂停通常在微秒级,大幅优于 G1(通常在几十到几百毫秒),并且分代 ZGC 的停顿时间不受堆大小影响。

  2. 全并发:通过染色指针+读写屏障的支持,实现几乎所有阶段(标记、整理/压缩、对象回收)都和应用线程并发执行,STW 次数少,停顿时间短,最大程度保证业务不中断。

  3. 自适应触发:默认不进行任何配置情况下,ZGC 基于内存余量和申请速度自适应判断是否触发GC。当 JVM 检测到剩余可用堆不足以撑到下一次 GC,或者分配速率快于回收速率(年轻代空间即将耗尽)时主动发起垃圾回收,以避免因为分配失败导致应用线程阻塞。全堆内存达到一定阈值,自动决定是否触发回收,也可以通过参数手动干预或配置定时回收。

为什么出现背景中描述的现象

  1. 没有开启提前回收和定时回收:每次都是被动快打满才回收
  2. 测试环境对象申请速度慢:内存稳步增长,几乎半小时才达到一次阈值

三、ZGC优势

  1. 大堆高性能:能够支撑极大堆的 GC 而不会延长停顿时间(单次不超过1ms),稳定且性能高。

  2. 工业实践证明:转转和京东实践表明:

    • CPU 平均使用率上涨 20%
    • GC Allocation Stall 降低 85%
    • QPS 提升15%
    • 最大内存使用率基本不变
    • TP 响应时间降低
  3. 超大内存支持:X86_64 处理器地址线有 48 条,除去 4 位染色指针,剩余 44 位可用对象地址,理论上支持 16TB 内存(但官方宣称最大支持 4TB,目的是平衡性能、稳定性和实际需求)

  4. 低延迟场景最佳选择:适用于追求极致低延迟的应用场景、对业务的影响极小

  5. 自调优能力强:绝大多数场景无需复杂参数配置

四、ZGC参数调优

核心参数

  1. -XX:+ZProactive:在尚未发生内存压力前,根据分配趋势主动提前发起一次并发回收,避免被动等待内存接近填满。

  2. -XX:ZAllocationSpikeTolerance=2:控制 ZGC 对内存分配高峰(allocation spike)的敏感度,表示一般情况下,分配速率可以有两倍的暂时抖动被容忍。

    • 如果想要 JVM 更加积极和频繁地触发 GC,可以调小该值
    • 如果存在瞬时的剧烈高峰,可适当调大该值,让 ZGC 对突发分配更容忍,不至于频繁 GC
    • 但风险是可能老年代更快耗尽,或者最终发生 Full GC
  3. -XX:ZCollectionInterval=30:每 30 秒触发一次 GC,可以避免长时间不触发GC导致的内存堆积问题。

  4. -XX:ConcGCThreads=4 -XX:-UseDynamicNumberOfGCThreads

    • 固定 GC 线程数为4个
    • 不开启动态 GC 线程数
    • 稳定 GC 对系统资源的占用
    • 减少 GC 对正常业务线程的竞争

image.png

内存分配相关参数

  1. -XX:+UseNUMA:在NUMA架构的服务器上优化内存分配

  2. -XX:ZUncommitDelay=300:设置内存释放延迟,单位为秒,默认300秒

  3. -XX:ZUncommit=false/true:是否允许ZGC将未使用的内存返还给操作系统

调试和监控参数

  1. -XX:+UnlockDiagnosticVMOptions -XX:+ZStatisticsForceTrace:强制ZGC输出更详细的统计信息,用于调试

  2. -Xlog:gc*=debug:file=gc.log:time,pid,tags:filecount=10,filesize=100m:详细GC日志配置

五、实战经验与最佳实践

适用场景

  1. 低延迟敏感型应用:如金融交易、在线游戏、实时数据处理等对响应时间有严格要求的系统
  2. 大内存应用:8GB以上堆内存的应用可以考虑使用ZGC
  3. 高吞吐量与低延迟兼顾:虽然吞吐量略低于Parallel GC,但差距不大,同时可提供极低的延迟

不适用场景

  1. 内存受限系统:ZGC内存开销较大,不适合内存较小的环境
  2. CPU受限系统:ZGC会增加约15-20%的CPU使用率
  3. 极端吞吐量优先场景:如果只关心吞吐量而不关心延迟,Parallel GC可能更合适

实施建议

  1. 循序渐进:对于生产环境,建议先在测试/预发布环境充分验证ZGC表现
  2. 监控到位:上线ZGC后应重点监控JVM GC指标、系统CPU使用率、应用延迟等关键指标
  3. 容器环境注意事项:确保容器内存限制大于JVM堆内存+额外开销(通常为堆内存的15%)
  4. 调参谨慎:除非出现明显问题,否则不建议过多调整ZGC参数,其自适应能力已经很强
  5. 升级路径:堆不大且有G1调优经验的情况下,建议还是使用G1,等ZGC有更多更佳实践之后再考虑迁移

六、结论与建议

针对本次问题,建议采取以下优化措施:

  1. 启用ZGC提前回收机制:-XX:+ZProactive
  2. 设置定期回收:-XX:ZCollectionInterval=30(根据业务特性调整)
  3. 合理设置容器内存限制:确保至少比Java堆内存大20%
  4. 在实际业务负载下进行压力测试,验证优化效果
  5. 适当调整ZAllocationSpikeTolerance参数,根据内存分配模式找到最佳平衡点

ZGC作为新一代低延迟垃圾收集器,具有极大的潜力,特别是在JDK 21引入分代ZGC后,性能和适用性都得到了显著提升。

随着更多企业实践的积累和官方的持续优化,ZGC有望在JDK 23成为默认垃圾收集器,成为Java应用的首选GC方案。